<?php

namespace pmvc\data\sql\pdo;

use pmvc\data\sql\DataSource;
use pmvc\log\Logger;
use pmvc\util\StringUtil;
use pmvc\util\ClassUtil;
use pmvc\util\ReflectionUtil;

use Exception;
use PDO;
use DateTime;

/**
 * PDO implementation of the {@link DataSource}
 * interface.
 *
 */
class PDODataSource
	implements DataSource {

	/**
	 * @var string
	 */
	public $dsn;

	/**
	 * @var string
	 */
	public $username;

	/**
	 * @var string
	 */
	public $password;

	/**
	 * @var Array
	 */
	public $driverOptions = Array();

	/**
	 * @var PDO
	 */
	private $link = FALSE;

	/**
	 * @var Logger
	 */
	private $logger = null;

	/**
	 * Creates the datasource.
	 * @param PDO $link
	 */
	public function __construct(PDO $link=null) {
		$this->logger = Logger::getLogger(__CLASS__);
		$this->link = $link;
	}
	
	/**
	 * Builds and binds a PDOStatement.
	 * @param string $sql
	 * @param array $args
	 */
	protected function buildAndBindStatement($sql, $args) {

		// anonymous function for building the query
		$params = Array();
		$fun = null;
		$fun = function($queryString, $args, $prefix="param_")use(&$params, &$fun) {
			return preg_replace_callback('/\{([0-9A-Za-z\._\-]+)\}/',
				function($matches)use($args, &$params, $prefix, &$fun) {
					$key 		= $matches[1];
					$value		= isset($args[$key]) ? $args[$key] : null;
					$ret		= ":".$prefix.$key;

					// handle arrays
					if (isset($value) && is_array($value)) {
						$ret = "";
						$first = true;
						$newArgs = Array();
						foreach ($value as $arrayKey => $arrayValue) {
							if (!$first) { $ret .= ", "; }
							$ret .= "{".$prefix.$key."_".$arrayKey."}";
							$newArgs[$prefix.$key."_".$arrayKey] = $arrayValue;
							$first = false;
						}
						$ret = $fun($ret, $newArgs, $prefix.$key."_");

					// null
					} else if (is_null($value)) {
						$params[$ret] = Array($value, PDO::PARAM_NULL);

					// string
					} else if (is_string($value)) {
						$params[$ret] = Array($value, PDO::PARAM_STR);

					// number
					} else if (is_numeric($value)) {
						$params[$ret] = Array($value, PDO::PARAM_INT);

					// boolean
					} else if (is_bool($value)) {
						$params[$ret] = Array($value, PDO::PARAM_BOOL);

					// datetime
					} else if (is_object($value) && ReflectionUtil::isInstance($value, 'DateTime')) {
						$params[$ret] = Array($value->format("Y-m-d H:i:s"), PDO::PARAM_STR);

					// unknown
					} else {
						throw new Exception(
						    "Unable to determine variable type for key ".$key." of sql query");
					}

					// return it
					return $ret;
				}, $queryString);
		};

		// parse the args and update the sql
		$sql = $fun($sql, $args);

		// prepare the statement
		if ($this->logger->canLog(Logger::TRACE)) {
	        $this->logger->trace("Preparing statement {0} with params {1}", $sql, print_r($params, true));
		}
		$ret = $this->link->prepare($sql);

		// bind it
		foreach ($params as $key => $val) {
			$ret->bindParam($key, $val[0], $val[1]);
		}

		// return it
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	public function createTransaction() {
		return new PDOTransaction($this->link);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function connect() {
		if (!isset($this->link)) {
			$this->link = new PDO(
				$this->dsn, $this->username, $this->password, $this->driverOptions);
		}
		$this->link->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function escape($str) {
		$ret = $this->link->quote($str);
		if ($ret===false) {
			$this->logger->warn("Configured PDO drvier doesn't support escape, making a poor attempt at doing it");
			$ret = str_replace("'", "''", $str);
		} else {
			$ret = substr($ret, 1, strlen($ret)-2);
		}
		return $ret;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function query() {
		$a		= func_get_args();
        $sql 	= array_shift($a);
        $res 	= $this->buildAndBindStatement($sql, $a);
        $this->assert($res->execute(), "Query failed");
        return $res;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function queryParams($sql, $params) {
        $res = $this->buildAndBindStatement($sql, $params);
        $this->assert($ret->execute(), "Query failed");
        return $res;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function update() {
		call_user_func_array(array($this,"query"), func_get_args());
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function updateParams($sql, $params) {
        $res = $this->buildAndBindStatement($sql, $params);
        $this->assert($res->execute(), "Query failed");
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function querySingle() {
		$res = call_user_func_array(array($this,"query"), func_get_args());
		$ret = $res->fetch(PDO::FETCH_OBJ);
		$res->closeCursor();
		return $ret;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function querySingleParams($sql, $params) {
        $res = $this->buildAndBindStatement($sql, $params);
        $this->assert($res->execute(), "Query failed");
		$ret = $res->fetch(PDO::FETCH_OBJ);
		$res->closeCursor();
		return $ret;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function queryMultiple() {
		$res = call_user_func_array(array($this,"query"), func_get_args());
		$ret = $res->fetchAll(PDO::FETCH_OBJ);
		$res->closeCursor();
		return $ret;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function queryMultipleParams($sql, $params) {
        $res = $this->buildAndBindStatement($sql, $params);
        $this->assert($res->execute(), "Query failed");
		$ret = $res->fetchAll(PDO::FETCH_OBJ);
		$res->closeCursor();
		return $ret;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function lastInsertId() {
		return $this->link->lastInsertId();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function numberOfRows($res) {
		return $res->rowCount();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function numberOfAffectedRows($res) {
		return $res->rowCount();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function fetchRowObject($res) {
		return $res->fetch(PDO::FETCH_OBJ);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function fetchRowObjects($res) {
		return $res->fetchAll(PDO::FETCH_OBJ);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function freeResult($res) {
		$res->closeCursor();
	}

	/**
	 * {@inheritDoc}
	 */
	public function getLastError() {
		$errorInfo = $this->link->errorInfo();
		return "errno ".$errorInfo[0].":".$errorInfo[1]." - ".$errorInfo[2];
	}

	/**
	 * @param bool $value
	 * @param string $message
	 */
	private function assert($value, $message="") {
		if ($value!==true) {
			throw $this->createExceptionFromLastError($message);
		}
	}

	/**
	 * @param string $message
	 * @param Exception $previous
	 */
	private function createExceptionFromLastError($message="", $previous=null) {
		return new Exception(
			$message." - ".$this->getLastError(),
			$previous);
	}

	/**
	 * Optionally sets the PDO that this DataSource will use.
	 * @param PDO $pdo
	 */
	public function setPdo(PDO $pdo) {
		$this->link = $pdo;
	}

}

?>